iT邦幫忙

2022 iThome 鐵人賽

DAY 22
0
Modern Web

致 JavaScript 開發者的 Functional Programming 新手指南系列 第 22

Day 22 :什麼是 Currying(4)?自己動手寫一個 Curry 吧!

  • 分享至 

  • xImage
  •  

在先前的文章中,我們花了很多的時間來討論閉包,這是為什麼呢?因為在 FP 中,如果我們想要更有效率、更嚴謹的方法來撰寫函式,了解函式在呼叫堆疊中的運行模式可以說是非常重要的,因為這是我們要使用「科里化」來優化函式的必備知識。

看到這邊的大家,可能會覺得:「咦?不是已經有純函式了嗎?」

難道純函式還不夠嚴謹嗎?

在介紹科里化之前,讓我們來回顧一下上一章節的範例:

const makeName = (name) => `My name is ${name}`;
const newName = makeName('Vivian');
console.log(newName);
// My name is Vivian

在上方的範例中,我們撰寫了一個名字產生器,只要帶入一個參數,就可以產出帶有參數的指定字串,但問題來了,如果我們要帶入不只一個參數呢?如果我們想要傳入多個名字的話:

const makeName = (firstName, secondName) => `First name is ${firstName}; second name is ${secondName}.`;
const newName = makeName('Vivian', 'Joe');
console.log(newName);
// First name is Vivian; second name is Joe.

這裡乍看之下好像沒有什麼問題,如果此時有另外一個人想要使用這個函式,但是不小心忘記帶入第二個參數,這時候會發生什麼事呢?

const makeName = (firstName, secondName) => `First name is ${firstName}; second name is ${secondName}.`;
const newName = makeName('Vivian');
console.log(newName);
// newName = ?

沒錯,由於我們並沒有帶入第二個參數,所以 secondName 在函式運行時無法被參照,所以成了 undefined ,最後輸出的結果會是:

// First name is Vivian; second name is undefined.

當然也有人會覺得:「啊!那就給定預設值就好了呀?」如果給定預設值的話,我們函式會長成:

const makeName = (firstName, secondName='') => `First name is ${firstName}; second name is ${secondName}.`;
const newName = makeName('Vivian');
console.log(newName);
// First name is Vivian; second name is.

這樣的函式顯然美中不足,雖然它已經符合純函式的規範,但我們可能還要額外撰寫一堆判斷式來決定我們的回傳值,也依然沒有解決使用者不一定知道在函式中要帶入幾個參數的問題,而且可想而知地,函式的可複用性又是另一個問題。

這就是前文我們所提到,為什麼 純函式依然有他的問題的存在的原因,但沒有關係,這個問題我們可以很好地透過「科里化」來解決。

什麼是「柯里化」?

其實最初科里化(Currying)是一個數學理論,後續才由 Haskell Curry 發揚光大,故名「Currying」。

科里化的核心概念是將函式要帶入的多個參數,轉換為一次性帶入一個參數,舉例來說:

// 此處函式 f 僅示意某個需要帶入多個函式之參數
const a = f(arg1, arg2, arg3); 
// Currying 後
const a = curriedF(arg1);
const b = a(arg2);
const c = b(arg3);
// 也可以進行鏈型串接
const chain = curriedF(arg1)(arg2)(arg3);

這麼做有什麼好處呢?讓我們來看看科里化與純函式相比有什麼好處:

  1. 可以把程式法切的更碎片,提高複用性,例如:可以固定第一個、第 n 個參數的值,根據需求將關注點放在需要放的地方。
  2. 因為一次只傳入一個參數,降低傳入參數數量的錯誤

難道上述兩個優點純函式做不到嗎?是的純函式還真的做不到,讓我們直接來看範例比較:

//  如果我們要用純函式固定第一個參數:
const a = f(arg1, arg2, arg3);
const b = f(arg1, arg4, arg5);
// 透過科里化固定參數:
const c =  curriedF(arg1);
const d = c(arg2)(arg3);
const e = c(arg4)(arg5);

我們會發現若是透過純函式固定第一個參數,其實沒什麼意義,因為我們還是會不斷重複帶入同樣的參數,光是上方的程式碼,就足以想見未來若是要重複使用函式,未經科里化的 Pure Funciton 程式碼會越來越多。

但經過先前的介紹,我們其實完全可以透過 JavaScript 執行堆疊的特性,降低重複傳入固定參數的次數,因為 JavaScript 本身就自帶保留參數的機制,我們就更應該好好使用。

撰寫柯里化函式

既然了解科里化的好處,就讓我們把之前的名字產生器做一個更好的優化吧:

const makeName = (firstName) => (secondName) => `First name is ${firstName}; second name is ${secondName}.`;;
const makeFirstName = makeName('Vivian');
console.log(makeFirstName);
// output = ?

我們透過科里化的方式,將函式透過閉包的技巧留住每次傳入參數,當我們固定一個參數時,輸出會長怎麼樣呢?

(secondName) => `First name is ${firstName}; second name is ${secondName}.`

由於我們還沒有帶入第二個參數,所以 makeFirstName 會回傳一個函式,此時讓我們來試試看,究竟第一個韓式有沒有被固定住:

const secondNameIsJoe = makeFirstName('Joe');
const secondNameIsTim = makeFirstName('Tim');
console.log(secondNameIsJoe);
console.log(secondNameIsTim);
// output 1 : 'First name is Vivian; second name is Joe.'
// output 2 : 'First name is Vivian; second name is Tim.'

很棒,第一個參數確實被固定住了,這樣就完成了我們的第一個科里化函式了!是不是比原本的純函式複用性更加高了呢?

當然,為了讓大家能更輕易理解科里化的好處及應用,這邊舉的例子相對簡單,但其實在實務開發中,可以透過這個機制完成更多更複雜的應用。

如果你是第一次接觸柯里化函式,可能會覺得需要花點時間去消化了解,那都是正常的,在下一個章節中,我們將要聊聊另外一個概念「高階函式」,來讓我們更加理解目前為止所學習到的工具,那麼就讓我們下一章節見吧!

參考資料:

  1. wikipedia - currying
  2. wikipedia - Haskell Curry

上一篇
Day 21 :什麼是 Currying(3)?JavaScript 閉包
下一篇
Day 23 :高階函數與複合函數(1):進階的函數應用
系列文
致 JavaScript 開發者的 Functional Programming 新手指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言